#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import subprocess
import os
import re
import uuid
import json
import difflib
from enum import Enum
from datetime import datetime
from common.const import RpcParamKey
from common.number_const import NumberConst
from common.common import output_execution_result_ex
from common.env_common import adaptation_win_path

from adds.comm.log import log
from adds.comm.const import ADDSCode
from adds.comm.common import execute_cmd


class CommandResult(str, Enum):
    SUCCESS = "0"


# 不支持修改的参数
disable_change_list = [
    "badPasswordTime", "badPwdCount", "CanonicalName", "CN", "Created", "whenChanged", "ObjectGuid", "groupType",
    "createTimeStamp", "Deleted", "DistinguishedName", "dSCorePropagationData", "instanceType", "ObjectGUID", "ou",
    "pwdLastSet", "isDeleted", "LastKnownParent", "lastLogoff", "lastLogon", "logonCount", "memberOf", "Modified",
    "whenCreated", "modifyTimeStamp", "nTSecurityDescriptor", "ObjectCategory", "PropertyNames", "objectSid", "member",
    "primaryGroupID", "PropertyCount", "sAMAccountType", "sDRightsEffective", "PropertyCount", "msNPAllowDialin",
    "ProtectedFromAccidentalDeletion", "msDS-LastKnownRDN", "uSNChanged", "lockoutTime", "userParameters", "Name",
    "lastLogonTimestamp", "msExchMailboxGuid", "msExchMailboxSecurityDescriptor", "msExchWhenMailboxCreated",
    "mDBUseDefaults", "mS-DS-CreatorSID"
]

disable_create_list = [
    "badPasswordTime", "badPwdCount", "CanonicalName", "CN", "Created", "whenChanged", "ObjectGuid", "member",
    "createTimeStamp", "Deleted", "DistinguishedName", "dSCorePropagationData", "instanceType", "ObjectGUID",
    "pwdLastSet", "isDeleted", "LastKnownParent", "lastLogoff", "lastLogon", "logonCount", "memberOf", "Modified",
    "whenCreated", "modifyTimeStamp", "nTSecurityDescriptor", "ObjectCategory", "PropertyNames", "objectSid",
    "primaryGroupID", "PropertyCount", "sAMAccountType", "sDRightsEffective", "PropertyCount", "msNPAllowDialin",
    "ProtectedFromAccidentalDeletion", "msDS-LastKnownRDN", "uSNChanged", "lockoutTime", "userParameters",
    "userAccountControl", "lastLogonTimestamp", "msExchMailboxGuid", "msExchMailboxSecurityDescriptor",
    "mDBUseDefaults", "msExchWhenMailboxCreated", "mS-DS-CreatorSID"
]

# 只能使用-Clear修改的参数
clear_list = [
    "msNPAllowDialin", "userParameters", "msDS-SupportedEncryptionTypes", "msRADIUS-FramedIpv6Route",
    "msRADIUS-SavedFramedIpv6Prefix", "msRADIUS-FramedIpv6Prefix", "msRADIUS-SavedFramedIpv6Route"
]


def get_win_dir():
    """
    功能描述: 适配windows下特殊场景路径
    参数：无
    """
    head_path = os.environ['SystemRoot']
    if head_path:
        return head_path.replace("\\", "/")
    return ""


def get_powershell_path():
    """
    功能描述: 获取powershell工具路径
    参数：无
    """
    return f"{get_win_dir()}/system32/WindowsPowerShell/v1.0/powershell"


def check_powershell_result(ps_command, expect_word):
    ps_loc = get_powershell_path()
    process = subprocess.Popen([ps_loc, ps_command], stdout=subprocess.PIPE)
    # 实时获取输出
    while True:
        output = process.stdout.readline()
        if output == b'' and process.poll() is not None:
            break
        if output:
            output_str = output.strip().decode('utf-8')
            if expect_word in output_str:
                return True
    return False


def report_job_details_by_rpc(req_id, job_id, job_detail: dict, interface_name="ReportJobDetails"):
    """
    功能描述：上报进度
    参数：无
    @job_id: 任务id
    @job_detail: 任务详情
    返回值：无
    """
    rpc_tool_path = f"{adaptation_win_path()}/DataBackup/ProtectClient/Plugins/GeneralDBPlugin/bin/rpctool.bat"
    random_id = f'adds_{req_id}_' + str(uuid.uuid4())
    input_file = RpcParamKey.WINDOWS_PARAM_FILE_PATH + "/" + random_id + "_input"
    output_file = RpcParamKey.WINDOWS_RESULT_PATH + "/" + random_id + "_output"
    # 将任务状态写入文件
    output_execution_result_ex(input_file, job_detail)
    cmd = f"{rpc_tool_path} {interface_name} {input_file} {output_file}"
    code, out, err = execute_cmd(cmd)
    log.info(f'{interface_name}, code: {code}, out: {out}, err: {err}')
    with open(output_file, 'r', encoding='utf-8') as file_read:
        result = json.load(file_read)
    remove_file(input_file)
    remove_file(output_file)
    ret = result.get("code", -1)
    if ret != ADDSCode.SUCCESS.value:
        log.error(f"{interface_name} failed, job id: {job_id}, ret: {ret}")
        return False
    else:
        log.info(f"{interface_name} success, job id: {job_id}")
        return True


def remove_file(file_path):
    if os.path.exists(file_path):
        os.remove(file_path)
    else:
        log.info(f"{file_path} is not exist, needn't remove")


def convert_copy_id(copy_id):
    return str(uuid.uuid3(uuid.NAMESPACE_OID, copy_id))[:8]


def get_all_children_nodes(path, obj, children_nones):
    if not os.path.exists(path):
        return
    files = os.listdir(path)
    children_nones.add(obj)
    if len(files) == 0:
        return

    for file in files:
        child_path = path + "/" + file
        if os.path.isdir(child_path):
            get_all_children_nodes(child_path, obj + "/" + file, children_nones)
    return


def parse_object_id(param_dict_list, info, object_from):
    if object_from == '0':
        info_to_json = json.loads(info)
        name_from_json = info_to_json.get("name", "")
        for obj_dict in param_dict_list:
            if obj_dict.get("CanonicalName", "") == name_from_json:
                return obj_dict.get("ObjectGUID", ""), obj_dict.get("DistinguishedName", "")
    else:
        name_from_json = json.loads(info).get("name", "")
        type_from_json = json.loads(info).get("type", "")
        for obj_dict in param_dict_list:
            if obj_dict.get("CN", "") == name_from_json and obj_dict.get("ObjectClass", "") == type_from_json:
                return obj_dict.get("ObjectGUID", ""), obj_dict.get("DistinguishedName", "")
    return "", ""


def parse_object_info(param_dict_list, obj_id):
    obj_dict = get_obj_param_info(param_dict_list, obj_id)
    param_dict = dict()
    if not obj_dict:
        return param_dict
    for key, value in obj_dict.items():
        if key in disable_create_list or not value:
            continue
        if key == "userAccountControl":
            value = 544
        param_dict[key] = format_value(value)
    return param_dict


def get_obj_param_info(param_dict_list, obj_id):
    log.info("get_obj_param_info")
    for obj_dict in param_dict_list:
        if obj_dict.get("ObjectGUID", "") == obj_id:
            return obj_dict
    log.warn(f"obj_id:{obj_id} not found")
    return dict()


def get_obj_path(param_dict_list, obj_id):
    for obj_dict in param_dict_list:
        if obj_dict.get("ObjectGUID", "") == obj_id:
            distinguishedname = obj_dict["DistinguishedName"]
            index = distinguishedname.find(',')
            result = distinguishedname[index + 1:]
            return result
    log.warn(f"obj_id:{obj_id} not found")
    return ""


def check_obj_param_info(old_info, new_info):
    param_diff = list()
    param_add = list()
    param_del = list()
    param_clear = list()
    add_keys = set(new_info.keys()) - set(old_info.keys())
    for add_key in add_keys:
        if add_key in clear_list:
            param_clear.append(add_key)
            continue
        if add_key in disable_change_list:
            continue
        param_add.append(f"'{add_key}'={format_value(new_info[add_key])}")
    del_keys = set(old_info.keys()) - set(new_info.keys())
    for del_key in del_keys:
        if del_key in disable_change_list:
            continue
        param_del.append(f"'{del_key}'={format_value(old_info[del_key])}")

    for old_param_key in old_info.keys():
        if old_param_key in disable_change_list:
            continue
        if old_param_key not in new_info.keys():
            continue
        if old_info[old_param_key] is None and new_info[old_param_key] is not None:
            param_add.append(f"'{old_param_key}'={format_value(new_info[old_param_key])}")
            continue
        if old_info[old_param_key] != new_info[old_param_key]:
            param_diff.append(f"'{old_param_key}'={format_value(old_info[old_param_key])}")
    param_info = [param_add, param_del, param_diff, param_clear]
    return param_info


def get_ad_domain():
    ps_loc = get_powershell_path()
    cmd = "Get-ADDomain | foreach {$_.DNSRoot}"
    code, out, err = execute_cmd([ps_loc, cmd], timeout=60, cmd_array_flag=True)
    if code == "0":
        return str(out.strip())
    return ""


def get_ad_domain_distinguishedname():
    ps_loc = get_powershell_path()
    cmd = "ipconfig /all"
    code, out, err = execute_cmd([ps_loc, cmd], timeout=60, cmd_array_flag=True)
    if code != "0":
        return ""
    result = str(out.strip())
    for line in result.splitlines():
        dns_flag = get_ad_dns_flag(line)
        if dns_flag:
            domain_name = line.strip().split(":")[-1].strip()
            domain_distinguishedname = format_domain_distinguishedname(domain_name)
            log.info(f"ad_domain_distinguishedname is {domain_distinguishedname}")
            return domain_distinguishedname
    return ""


def get_ad_dns_flag(line):
    if (line.strip().startswith("Primary Dns Suffix") or line.strip().startswith("Suffixe DNS principal")):
        return True
    elif (line.strip().startswith("Sufixo DNS primário") or line.strip().startswith("主 DNS 后缀")):
        return True
    else:
        return False


def format_domain_distinguishedname(domain):
    dc_list = domain.split(".")
    domain_distinguishedname = ""
    for dc in dc_list:
        domain_distinguishedname += f"DC={dc},"
    if len(domain_distinguishedname) > 0:
        domain_distinguishedname = domain_distinguishedname[:-1]
    return domain_distinguishedname


def is_normal_mode():
    cmd = "bcdedit /enum"
    ps_loc = get_powershell_path()
    code, out, err = execute_cmd([ps_loc, cmd], timeout=120, cmd_array_flag=True)
    if code == "0" and "safeboot" not in out.strip():
        return True
    return False


def get_previous_task_info():
    log.info("get task info.")
    ps_loc = get_powershell_path()
    cmd = "Get-WBJob -Previous 1 | ConvertTo-Json"
    code, out, err = execute_cmd([ps_loc, cmd], timeout=60, cmd_array_flag=True)
    if code != CommandResult.SUCCESS.value:
        return dict()
    task_info = json.loads(out.strip())
    return task_info


def format_value(value):
    if isinstance(value, list):
        value_str = ",".join(['"{0}"'.format(item) for item in value])
        value_str = value_str.replace("`", "``")
        value_str = value_str.replace("$", "`$")
    else:
        value = str(value)
        value = value.replace("`", "``")
        value = value.replace("$", "`$")
        value = value.replace("\"", "`\"")
        value_str = f"\"{value}\""

    return value_str


def bcdedit_delete_safeboot():
    """
    设置主机重启后为正常模式
    """
    log.info("bcdedit delete safeboot.")
    cmd = "bcdedit /deletevalue safeboot"
    ps_loc = get_powershell_path()
    execute_cmd([ps_loc, cmd], timeout=120, cmd_array_flag=True)


def checkout_recycle_bin_feature():
    ps_loc = get_powershell_path()
    check_cmd = "Get-ADOptionalFeature \"Recycle Bin Feature\" | select-object EnabledScopes"
    code, out, err = execute_cmd([ps_loc, check_cmd], timeout=60, cmd_array_flag=True)
    if code == CommandResult.SUCCESS.value:
        if "{}" not in out:
            log.info("Recycle Bin Feature already enabled.")
            return True
    else:
        log.error(f"check_code:{code},checkout_out:{out},check_err:{err}")
        return False
    return False


def check_need_to_move(old_info, new_info):
    log.info("check object Whether need to move")
    if "DistinguishedName" not in old_info.keys() or "DistinguishedName" not in new_info.keys():
        return False, ""
    old_distinguishedname = old_info["DistinguishedName"]
    new_distinguishedname = new_info["DistinguishedName"]
    if old_distinguishedname != new_distinguishedname:
        index = old_distinguishedname.find(',')
        result = old_distinguishedname[index + 1:]
        return True, result
    return False, ""


def check_need_change_memberof(old_info, new_info):
    log.info("check object Whether need to change memberof")
    add_list = list()
    delete_list = list()
    if "memberOf" not in old_info.keys() and "memberOf" not in new_info.keys():
        return False, add_list, delete_list

    if "memberOf" in old_info.keys() and "memberOf" not in new_info.keys():
        add_list.extend(old_info["memberOf"])
        return True, add_list, delete_list

    if "memberOf" not in old_info.keys() and "memberOf" in new_info.keys():
        delete_list.extend(new_info["memberOf"])
        return True, add_list, delete_list

    if "memberOf" in old_info.keys() and "memberOf" in new_info.keys():
        set1 = set(old_info["memberOf"])
        set2 = set(new_info["memberOf"])
        diff1 = set1.difference(set2)
        add_list.extend(list(diff1))
        diff2 = set2.difference(set1)
        delete_list.extend(list(diff2))
        return True, add_list, delete_list

    return False, add_list, delete_list


def check_need_change_member(old_info, new_info, object_json_dict_list, id_info):
    log.info("check object Whether need to change member")
    add_list = list()
    delete_list = list()
    change_list = list()
    if "member" not in old_info.keys() and "member" not in new_info.keys():
        return False, [add_list, delete_list, change_list]

    if "member" in old_info.keys() and "member" not in new_info.keys():
        member_add = get_obj_exist_with_distinguishedname(old_info["member"], object_json_dict_list, id_info)
        add_list.extend(member_add)
        return True, [add_list, delete_list, change_list]

    if "member" not in old_info.keys() and "member" in new_info.keys():
        delete_list.extend(new_info["member"])
        return True, [add_list, delete_list, change_list]

    if "member" in old_info.keys() and "member" in new_info.keys():
        set1 = set(old_info["member"])
        set2 = set(new_info["member"])
        if set1 != set2:
            member_change = get_obj_exist_with_distinguishedname(old_info["member"], object_json_dict_list, id_info)
            change_list.extend(member_change)
        return True, [add_list, delete_list, change_list]

    return False, [add_list, delete_list, change_list]


def check_need_to_rename(old_info, new_info):
    log.debug("check object Whether need to rename")
    if "Name" not in old_info.keys() or "Name" not in new_info.keys():
        return False, ""
    old_name = old_info["Name"]
    new_name = new_info["Name"]
    if old_name != new_name:
        return True, format_value(old_name)
    return False, ""


def get_obj_exist_with_distinguishedname(identity_list, object_json_dict_list, id_info):
    target_list = list()
    for identity in identity_list:
        _, obj = get_ad_object_info(object_json_dict_list, "memberOf", identity)
        ps_loc = get_powershell_path()
        check_cmd = f"Get-ADObject -Identity \"{obj}\" -Properties * | ConvertTo-Json"
        log.debug(f" the check cmd of  wheobj_exist_with_distinguishedname:{check_cmd}")
        code, out, err = execute_cmd(cmd=[ps_loc, check_cmd], timeout=120, cmd_array_flag=True)
        if code == "0":
            ad_object = json.loads(out.strip())
            obj_id = ad_object.get('ObjectGUID')
            target_list.append(obj_id)
            continue
        if identity in id_info.keys():
            target_list.append(id_info[identity])
    return target_list


def get_ad_object_info(object_json_dict_list, key="ObjectGUID", value=""):
    for object_json in object_json_dict_list:
        if key in object_json.keys() and value in object_json.get(key):
            return dict(object_json), value
    return dict(), ""


def scan_dir_size(dir_path):
    total_size = 0
    for dir_path, _, filenames in os.walk(dir_path):
        for f in filenames:
            fp = os.path.join(dir_path, f)
            total_size += os.path.getsize(fp)
    return int(total_size / 1024)


def check_task_in_current_time(start, end, task_begin_time):
    try:
        ps_loc = get_powershell_path()
        start = int(start / 60) * 60
        get_timestamp_cmd = f"Get-Date '{task_begin_time}' -Format \"yyyy-MM-dd HH:mm:ss\""
        code, out, err = execute_cmd(cmd=[ps_loc, get_timestamp_cmd], timeout=120, cmd_array_flag=True)
        if code == "0":
            date_str = out.strip()
            date_obj = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S')
            timestamp = int(date_obj.timestamp())
            if start <= timestamp < end:
                return True
        else:
            log.error(f"code:{code},out:{out},err:{err}")
    except Exception as err:
        log.error(f"check_task_in_current_time exception, err: {err}.")
    log.error(f"current task info not find.")
    return False


def get_decoding_mode():
    ps_loc = get_powershell_path()
    result = subprocess.Popen([ps_loc, 'chcp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
    stdout, stderr = result.communicate(timeout=NumberConst.THIRTY)
    if stderr:
        log.error(f"error Occurred: {stderr}")
        return ''
    pattern = r"(\d+)"
    code_page = re.search(pattern, stdout)
    if code_page:
        code_page = code_page.group(1)
        decoding = 'cp' + code_page
        log.info(f"The encoding mode of the system is: {decoding}")
    else:
        decoding = 'utf-8'
    return decoding
